package com.google.code.diffusion;

import java.security.InvalidParameterException;
import java.util.Arrays;

import android.content.Context;

public class BankNote {

  // -- PRIVATE DATA ----------------------------------------------------------
                    
  private final Context context; /** The application context this class runs in. */
  
  // Data fields.
  private String amount;     /** The value the banknote represents. */
  private String serial;     /** The serial code found on the banknote. */
  private String short_code; /** The short code found on the banknote. */
  private String comment;    /** The comment the user wished to add to this
                                 submission. */
  private String country;    /** The country where the note was found. */
  private String city;       /** The city where the note was found. */
  private String postal;     /** The postal/zip code where the note was found. */
  
  /** The country code used in the serial numbers, which can be matched to
   *  the checksum values.
   *  
   *  It would seemingly be more logical to use a HashMap for that, but a flat
   *  list doesn't have as much initialization overhead and makes it possible to
   *  link to other lists in the future, for example with information about the
   *  country.
   *  
   *  WARNING: List should be sorted! */
  public final static char[] SERIAL_COUNRTY_CODES = 
    {'E', 'F', 'G', 'H', 'L', 'M', 'N', 'P', 'S', 'T', 'U', 'V', 'X', 'Y', 'Z'};
  
  /** The checksum values used in the serial numbers. The index numbers
   *  correspond with those in SERIAL_COUNTRY_CODES. */
  public final static int[] SERIAL_CHECKSUM_VALUES =
    {3, 2, 1, 9, 5, 4, 3, 1, 7, 6, 5, 4, 2, 1, 9};

  /** The country codes used in the short codes. */
  public final static char[] SHORT_COUNTRY_CODES = 
    {'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'T', 'U'};

  /** Allowed values for the banknotes. */
  public final static String[] BANKNOTE_VALUES = 
    {"5", "10", "20", "50", "100", "200", "500"};
  
  // -- PUBLIC METHODS --------------------------------------------------------
  
  /** Construct a new empty banknote. */
  public BankNote(Context application_context) {
    // Initialize the parameters.
    amount     = null;
    serial     = null;
    short_code = null;
    comment    = "";
    
    context = application_context;
  }

  /** Set the type of banknote.
   *  @param amount the amount of euros the note represents.
   */
  public void setAmount(String amount) throws InvalidParameterException {
    this.amount = null;
    amount = amount.trim();
    if (Arrays.asList(BANKNOTE_VALUES).contains(amount)) {
      this.amount = amount;
    } else {
      throw new InvalidParameterException("There are no euro banknotes of value " + amount);
    }
  }

  /** Get the type of banknote.
   * 
   *  @throws IllegalStateException if type is not set.
   */
  public String getAmount() throws IllegalStateException {
    if (amount == null) {
      throw new IllegalStateException("Amount is not set");
    }
    return amount;
  }

  /** Set the serial code found on the banknote. This code is always made up of
   *  one letter followed by eleven digits. The code is checked for form and
   *  whether the checksum is correct.
   *  
   *  Allowed values for the letter, which determines where the note was
   *  printed, are Z, Y, X, V, U, T, S, P, N, M. L, H, G, F and E.
   *  
   *  @param code a serial number consisting of one letter and eleven digits.
   *  @throws InvalidParameterException if a format or value error is detected.
   *                                    Check the error message to find out the
   *                                    problem.
   */
  public void setSerial(String letter, String number) throws InvalidParameterException {
    // Reset serial.
    serial = null;
    
    // Internally, everything is treated in uppercase.
    letter = letter.trim().toUpperCase();
    
    // Letter should always be a single letter.
    if (letter.length() != 1)
      throw new InvalidParameterException("Expected one letter, got " + letter.length() + " instead");
    
    // Check for first char, which indicates the country of origin an should be
    // one of the allowed letters.
    char country_code = letter.charAt(0);
    int  country_index = Arrays.binarySearch(SERIAL_COUNRTY_CODES, country_code);
    if (country_index < 0)
      throw new InvalidParameterException("Unknown country code \"" + country_code + "\"");
    
    // Number should always consist of 11 numbers.
    number = number.trim();
    if (number.length() != 11)
      throw new InvalidParameterException("Expected 11 numbers, got " + number.length() + " instead");

    // Calculate the digital root of the number. This number should be a number 
    // specific to the country of origin and thus can be used as a checksum.
    long checksum;
    try {
      checksum = Long.parseLong(number, 10);
    } catch (NumberFormatException e) {
      throw new InvalidParameterException(number + " is not a number");
    }
    while (checksum > 9) {
      checksum = 1 + ((checksum - 1) % 9);
    }
    
    // Match the checksum to the country code.
    if (checksum != SERIAL_CHECKSUM_VALUES[country_index])
      throw new InvalidParameterException("Error in number (checksum fails)");
    
    serial = letter + number;
  }

  /** Return the serial number in uppercase.
   * 
   *  @throws IllegalStateException if serial number is not set.
   */
  public String getSerial() throws IllegalStateException {
    if (serial == null)
      throw new IllegalStateException("Serial is not set");
    return serial;
  }

  /** Set the short code, which contains info about the printer. It always
   *  consists of a letter to indicate the printer, three digits to indicate the
   *  plate, and a letter digit to indicate row and column on this plate. The
   *  code is checked for form and correctness and an InvalidParameterException
   *  is thrown when an error is found. 
   *  
   *  The allowed values for the first letter are D, E, F, G, H, J, K, L, M, N,
   *  P, R, T and U.
   *  @param code the short code, consisting of a letter, three numbers, a
   *              letter and a number.
   *  @throws InvalidParameterException if a format or value error is detected.
   *                                    Check the error message to find out the
   *                                    problem.
   */ 
  public void setShortCode(String country,
                           String plate,
                           String row,
                           String column) throws InvalidParameterException {
    // Reset short code.
    short_code = null;
    
    // Handle everything in uppercase internally.
    country = country.trim().toUpperCase();
    plate   = plate.trim().toUpperCase();
    row     = row.trim().toUpperCase();
    column  = column.trim().toUpperCase();
    
    // Check for the proper length of country code
    if (country.length() != 1)
      throw new InvalidParameterException("Expected one letter for country, got " + 
                                          country.length() + " instead");
   
    // Check for valid starting letter, which indicates the country of printing.
    if (Arrays.binarySearch(SHORT_COUNTRY_CODES, country.charAt(0)) < 0)
      throw new InvalidParameterException("Unknown country code \"" + country + "\"");
    
    // Check for the proper length of plate
    if (plate.length() != 3)
      throw new InvalidParameterException("Expected 3 numbers for plate, got " + 
                                          plate.length() + " instead");

    // Check for a valid number in the next three chars, which indicates the
    // plate number.
    try {
      Integer.parseInt(plate);
    } catch (NumberFormatException e) {
      throw new InvalidParameterException("\"" + plate + "\" is not a valid plate number");
    }
    
    // Check for the proper length of row
    if (row.length() != 1)
      throw new InvalidParameterException("Expected one letter for row, got " + 
                                          row.length() + " instead");
    
    // Check for valid row, which should be a letter.
    char row_chr = row.charAt(0);
    if ((row_chr <  'A') || (row_chr > 'Z'))
      throw new InvalidParameterException("\"" + row + "\" is not a valid row letter");
    
    // Check for the proper length of column
    if (column.length() != 1)
      throw new InvalidParameterException("Expected one number for column, got " + 
                                          column.length() + " instead");
    
    // Check for valid column, which should be a number.
    try {
      Integer.parseInt(column);
    } catch (NumberFormatException e) {
      throw new InvalidParameterException("\"" + column +
                                          "\" is not a valid column number");
    }
    
    short_code = country + plate + row + column;
  }

  /** Return the short code in uppercase.
   * 
   *  @throws IllegalStateException if the short code is not set.
   */
  public String getShortCode() throws IllegalStateException {
    if (short_code == null)
      throw new IllegalStateException("Short code is not set");
    return short_code;
  }

  /** Set the comment for this note.
   *  @param comment the free comment for the note
   */
  public void setComment(String comment) {
    this.comment = comment.trim(); 
  }

  /** Return the comment. */
  public String getComment() {
    return comment;
  }

  public void setLocation(String country,
                          String city,
                          String postal) throws InvalidParameterException {
    // Reset location.
    this.country = null;
    this.city    = null;
    this.postal  = null;
    
    // Test for empty country.
    country = country.trim();
    if (country.length() == 0)
      throw new InvalidParameterException("Country may not be empty");
    
    // Test if country is known, and if so, set it to the capitalization of
    // Eurobilltracker.
    String upper_country = country.toUpperCase();
    boolean found = false;
    for (String known_country : context.getResources().getStringArray(R.array.CountryArray)) {
      if (known_country.toUpperCase().equals(upper_country)) {
        country = known_country;
        found   = true;
        break;
      }
    }
    if (!found) {
      throw new InvalidParameterException("Unknown country \"" + country + "\"");
    }
    
    // Test for empty city.
    city = city.trim();
    if (city.length() == 0)
      throw new InvalidParameterException("City may not be empty");
    
    // Postal is treated uppercase.
    postal = postal.toUpperCase();
    
    // Test for empty postal.
    postal = postal.trim();
    if (postal.length() == 0) {
      if (country.equals("Ireland")) {
        postal = "IE";
      } else {
        throw new InvalidParameterException("Postal may not be empty"); 
      }
    }
    
    // Store new values.
    this.country = country;
    this.city    = city;
    this.postal  = postal;
  }
  
  /** Return the country or throw an IllegalStateException if no location is
   *  set. */
  public String getCountry() throws IllegalStateException {
    if (country == null)
      throw new IllegalStateException("Location is not set");
    return country;
  }

  /** Return the city or throw an IllegalStateException if no location is
   *  set. */
  public String getCity() throws IllegalStateException {
    if (city == null)
      throw new IllegalStateException("Location is not set");
    return city;
  }
  
  /** Return the postal or throw an IllegalStateException if no location is
   *  set. */
  public String getPostal() throws IllegalStateException {
    if (postal == null)
      throw new IllegalStateException("Location is not set");
    return postal;
  }

  /** Returns true if all neccesary fields in the banknote contain a valid
   *  value or false otherwise.  */
  public boolean ready() {
    return ((amount != null) && (serial != null) && (short_code != null) && 
            (country != null) && (city != null) && (postal != null));
  }

}
